package vg.modules.notepad.components.textComponent;

import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Observer;
import java.util.Stack;

import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextPane;
import javax.swing.SwingUtilities;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.plaf.ComponentUI;
import javax.swing.text.BadLocationException;
import javax.swing.text.Style;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;

import vg.services.main_manager.MainManager;

/**
 * This method realizes text component, which user doesn't edit and ...
 * @author tzolotuhin
 */
public class TextComponent implements ITextComponent {
	// Data
	private List<Anchor> anchors;
	// View
	private final JPanel view; 
	// Observers
	private final List<Observer>observers;
	// UI Components
	private final TextPaneWithHS textComponent;
	private final JScrollPane textScroll;
	private final TextLineNumber textLineNumber;
	private final TextSearchLine textSearchLine;
	private final ScrollPaneRowHeader header;
	// Defines
	private static final String DEF_FIND_TEXT = "FindTextColor";
	private static final String DEF_UNFIND_TEXT = "UnFindTextColor";
	/**
	 * Constructor.
	 */
	public TextComponent() {
		// data
		this.anchors = new ArrayList<TextComponent.Anchor>();
		// create components
		this.view = new JPanel(new GridBagLayout()); 
		this.textComponent = new TextPaneWithHS();
		this.textComponent.setBackground(Color.WHITE);
		this.textComponent.setEditable(false);
		this.textComponent.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR));
		// Consolas, Lucida Console, or other mono spaced fonts
		this.textComponent.setFont(new Font("Lucida Console", 0, 12));
		
		this.header = new ScrollPaneRowHeader();
		this.textLineNumber = new TextLineNumber(this.textComponent);
		this.textSearchLine = new TextSearchLine(this.textComponent);
		this.header.addRow(this.textLineNumber.getView());
		
		this.observers = new ArrayList<Observer>();
		// add attributes to text pane
		Style styleFindText = this.textComponent.addStyle(DEF_FIND_TEXT, null);
		StyleConstants.setBackground(styleFindText, Color.YELLOW);
		
		Style styleUnFindText = this.textComponent.addStyle(DEF_UNFIND_TEXT, null);
		StyleConstants.setBackground(styleUnFindText, this.textComponent.getBackground());
		// build ui
		
		this.textScroll = new JScrollPane(this.textComponent);
		this.textScroll.setRowHeaderView(this.header.getView());
		
		GridBagConstraints gbc = null;
		gbc = new GridBagConstraints(0,0, 1,1, 1,1, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0,0,0,0), 0,0);
		this.view.add(this.textScroll, gbc);

		gbc = new GridBagConstraints(1,0, 1,1, 0,1, GridBagConstraints.CENTER, GridBagConstraints.VERTICAL, new Insets(0,0,0,0), 0,0);
		this.view.add(this.textSearchLine.getView(), gbc);
		// add listeners		
		this.textComponent.addCaretListener(new CaretListener() {
			public void caretUpdate(CaretEvent e) {
				turnObservers();								
			}
		});
		this.textComponent.addKeyListener(new KeyAdapter() {
			public void keyPressed(KeyEvent e) {
				unfind();
				super.keyPressed(e);
			}
		});
	}
	///////////////////////////////////////////////////////////////////////////
	// IMPLEMENTATION OF ITEXTCOMPONENT
	///////////////////////////////////////////////////////////////////////////
	public synchronized JComponent getView() {
		return(this.view);
	}
	public synchronized int getColumnNumber() {
		int pos = this.textComponent.getCaretPosition();
		String text = this.textComponent.getText();
		int i = -1, oldPos = 0, countSpace = 0, space = 0;
		
		while(true) {
			i++;
			int unix = text.indexOf("\n", i);
			int win = text.indexOf("\r\n", i);
			if(win == -1 && unix == -1) {
				break;
			}
			if(unix < win || win == -1) {
				i = unix + 1;
				space++;
			} else {
				i = win + 2;
				space += 2;
			}
			if(i <= pos) {
				countSpace += space;
				oldPos = (i - countSpace);
			} else {
				break;
			}
		}
		text = null;
		return(pos - oldPos - countSpace + 1);
	}
	public synchronized int getLineNumber() {
		return(getLineNumber(this.textComponent.getCaretPosition()));
	}
	public synchronized void setText(String text) {		
		this.textComponent.setText(text);
		StyledDocument doc = (StyledDocument)this.textComponent.getDocument();
		try {
			String beginText = doc.getText(0, doc.getLength());
			String result = parsing(beginText);
			this.textComponent.setText(result);
			this.textLineNumber.setLineCount(getLineNumber(doc.getLength()));
			unfind();
			turnObservers();
		} catch (BadLocationException ex) {
			MainManager.logger.printException(ex);
		}
	}
	public synchronized void setTextSize(final int size) {
		Font font, newFont;
		font = this.textComponent.getFont();
		newFont = font.deriveFont((float)size);
		this.textComponent.setFont(newFont);
	}
	public synchronized void setTextBold(final boolean bold) {
		Font font = this.textComponent.getFont();
		if(bold) {
			Font newFont = font.deriveFont(Font.BOLD);
			this.textComponent.setFont(newFont);
		} else {
			Font newFont = font.deriveFont(0);
			this.textComponent.setFont(newFont);
		}
	}
	public synchronized void updateUITheme() {
		SwingUtilities.updateComponentTreeUI(this.view);
		// update attributes
		Style styleFindText = this.textComponent.addStyle(DEF_FIND_TEXT, null);
		StyleConstants.setBackground(styleFindText, Color.YELLOW);
		
		Style styleUnFindText = this.textComponent.addStyle(DEF_UNFIND_TEXT, null);
		StyleConstants.setBackground(styleUnFindText, this.textComponent.getBackground());
	}
	public void addObserver(Observer o) {
		synchronized (this.observers) {
			this.observers.add(o);
		}
	}

	public void deleteAllObservers() {
		synchronized (this.observers) {
			this.observers.clear();
		}
	}

	public void deleteObserver(Observer o) {
		synchronized (this.observers) {
			this.observers.remove(o);
		}
	}
	public synchronized Map<Integer, Integer> find(String whatFind) {
		unfind();
		Map<Integer, Integer>result = new HashMap<Integer, Integer>();
		StyledDocument doc = this.textComponent.getStyledDocument();
		String text = "";
		try {
			text = doc.getText(0, doc.getLength());
		} catch (BadLocationException ex) {
			MainManager.logger.printException(ex);
		}
		for(int i = 0; i < text.length(); i++) {
			i = text.indexOf(whatFind, i);
			if(i < 0) {
				break;
			} else {
				result.put(i, i + whatFind.length());
				doc.setCharacterAttributes(i, whatFind.length(), this.textComponent.getStyle(DEF_FIND_TEXT), true);
			}
		}
		this.textSearchLine.setSearchResult(result, text.length());
		return(result);
	}
	public synchronized void selectAnchor(final String anchor) {
		unfind();
		Map<Integer, Integer>result = new HashMap<Integer, Integer>();
		StyledDocument doc = this.textComponent.getStyledDocument();
		for(Anchor buf : this.anchors) {
			if(anchor != null && buf.getAnchorValue() != null && buf.getAnchorValue().equals(anchor)) {
				if(buf.getBeginPosition() == buf.getEndPosition()) {
					try {
						String str = doc.getText(buf.getBeginPosition(), doc.getLength() - buf.getBeginPosition());
						if(str != null) {
							int offset = str.indexOf('\n');
							if(offset < 0) offset = str.length();
							doc.setCharacterAttributes(buf.getBeginPosition(), offset, this.textComponent.getStyle(DEF_FIND_TEXT), true);
						}
					} catch (BadLocationException ex) {
						MainManager.logger.printException(ex);
					}
				} else {
					doc.setCharacterAttributes(buf.getBeginPosition(), buf.getEndPosition() - buf.getBeginPosition(), this.textComponent.getStyle(DEF_FIND_TEXT), true);
				}
				result.put(buf.getBeginPosition(), buf.getEndPosition());
			}
		}
		this.textSearchLine.setSearchResult(result, doc.getLength());
	}
	///////////////////////////////////////////////////////////////////////////
	// PRIVATE METHODS
	///////////////////////////////////////////////////////////////////////////
	private void turnObservers() {
		Object[] arrLocal;
		synchronized (this.observers) {
		    /* We don't want the Observer doing callbacks into
		     * arbitrary code while holding its own Monitor.
		     * The code where we extract each Observable from 
		     * the Vector and store the state of the Observer
		     * needs synchronization, but notifying observers
		     * does not (should not).  The worst result of any 
		     * potential race-condition here is that:
		     * 1) a newly-added Observer will miss a
		     *   notification in progress
		     * 2) a recently unregistered Observer will be
		     *   wrongly notified when it doesn't care
		     */
			arrLocal = this.observers.toArray();
		}		
		for (int i = 0 ; i < arrLocal.length ; i++) {
			((Observer)arrLocal[i]).update(null, null);
		}		
	}
	private int getLineNumber(int pos) {
		String text = this.textComponent.getText();
		int i = 0, line = 0;
		
		while(true) {
			int unix = text.indexOf("\n", i);
			int win = text.indexOf("\r\n", i);
			if(win == -1 && unix == -1) {
				break;
			}
			if(unix < win || win == -1) {
				i = unix + 1;
			} else {
				i = win + 2;
			}
			if(i <= pos) {
				line++;
			} else {
				break;
			}
		}
		text = null;
		return(line + 1);
	}
	private void unfind() {
		StyledDocument doc = this.textComponent.getStyledDocument();
		String text = this.textComponent.getText();
		doc.setCharacterAttributes(0, text.length(), this.textComponent.getStyle(DEF_UNFIND_TEXT), true);
		this.textSearchLine.reset();
	}
	/**
	 * This method finds all anchors. Then it saves and cuts them from beginText.
	 */
	private String parsing(String beginText) {
		final String def_anchor = "anchor";
		
		Stack<Anchor>stack = new Stack<Anchor>();
		this.anchors.clear();
		int count = 0, prev = 0, curr = 0;
		StringBuffer result = new StringBuffer(100);
		boolean check = false;
		for(int i = 0; i < beginText.length(); i++) {
			check = false;
			// find '<'
			int i1 = beginText.indexOf('<', i);
			int i2 = beginText.indexOf("</", i);
			if(i1 < 0) i = i2;
			if(i2 < 0) i = i1;
			if(i1 < i2 && i1 >= 0) i = i1;
			if(i2 <= i1 && i2 >= 0) i = i2;
			// find <anchor ...> or <anchor .../>
			if(i >= 0 && i == i1 && i1 != i2) {
				curr = i;
				// next find 'anchor'
				int j = passSpace(beginText, i+1);
				if((j+def_anchor.length()) < beginText.length()) {
					if(beginText.substring(j, j+def_anchor.length()).equals(def_anchor)) {
						// next find 'key', it's may be f1, f2 or other
						j = passSpace(beginText, j+def_anchor.length());
						if(j < beginText.length()) {
							int k1 = beginText.indexOf(' ', j);
							int k2 = beginText.indexOf('=', j);
							int k = k1;
							if(k2 < k1) k = k2;
							if(k >= 0) {
								String key = beginText.substring(j, k);
								// next find '='
								int l = passSpace(beginText, k);
								if(l < beginText.length() && beginText.charAt(l) == '=') {
									// next find 'value', it's may be link_128 or other
									j = passSpace(beginText, l+1);
									if(j < beginText.length()) {
										k1 = beginText.indexOf(' ', j);
										k2 = beginText.indexOf('/', j);
										int k3 = beginText.indexOf('>', j);
										if(k1 < k2 && k1 < k3) k = k1; else
										if(k2 < k1 && k2 < k3) k = k2; else
										if(k3 < k1 && k3 < k2) k = k3;
										String value = beginText.substring(j, k);
										// next find '>' or '\>'
										j = passSpace(beginText, k);
										if(j+1 < beginText.length() && beginText.charAt(j) == '/' && beginText.charAt(j+1) == '>') {
											// add new anchor
											this.anchors.add(new Anchor(key, value, i - count, i - count));
											// cut this tag from result text
											l = passSpaceAndEnter(beginText, j+2);
											check = true;
											count += (l-i);
											result.append(beginText.substring(prev, i));
											curr = i = l;
											i--;
										} else if(j < beginText.length() && beginText.charAt(j) == '>'){
											// find </anchor>
											l = passSpaceAndEnter(beginText, j+2);
											stack.push(new Anchor(key, value, i - count, -1));
											// cut this tag from result text
											result.append(beginText.substring(prev, i));
											check = true;
											count += (l-i);
											curr = i = l;
											i--;
										}
									}
								}
							}
						}
					}
				}
			} else if(i >= 0 && i == i2) {
				// find </anchor>
				int j = passSpace(beginText, i+2);
				if((j+def_anchor.length()) < beginText.length()) {
					if(beginText.substring(j, j+def_anchor.length()).equals(def_anchor)) {
						// next find '>'
						j = passSpace(beginText, j+def_anchor.length());
						if(j < beginText.length() && beginText.charAt(j) == '>') {
							int l = passSpaceAndEnter(beginText, j+1);
							if(!stack.isEmpty()) {
								// add new anchor
								Anchor a = stack.pop();
								this.anchors.add(new Anchor(a.getAnchorKey(), a.getAnchorValue(), a.getBeginPosition(), i - count));
								// cut this tag from result text
								check = true;
								count += (l-i);
								result.append(beginText.substring(prev, i));
								curr = i = l;
								i--;
							} else {
								// error
								MainManager.logger.printError("[" + this.getClass().getName() + ".parsing] [BAD] Count of </anchor> more then <anchor ...>");
								MainManager.windowMessage.warningMessage("Count of </anchor> more then <anchor ...>", "Parsing error");
							}
						}
					}
				}
			} else {
				result.append(beginText.substring(prev, beginText.length()));
				break;
			}
			if(check == false) {
				check = true;
				result.append(beginText.substring(prev, curr));
			}
			prev = curr;
		}
		return(result.toString());
	}
	private int passSpace(String text, int offset) {
		for(int i = offset; i < text.length(); i++) {
			if(text.charAt(i) != ' ' && text.charAt(i) != '\t') {
				return(i);
			}
		}
		return(text.length());
	}
	private int passSpaceAndEnter(String text, int offset) {
		for(int i = offset; i < text.length(); i++) {
			if(text.charAt(i) != ' ' && text.charAt(i) != '\t' && text.charAt(i) != '\n') {
				return(i);
			}
		}
		return(text.length());
	}	///////////////////////////////////////////////////////////////////////////
	// PRIVATE CLASSES
	///////////////////////////////////////////////////////////////////////////
	private static class TextPaneWithHS extends JTextPane {
		private final static long serialVersionUID = 1L;
		public TextPaneWithHS() {
			super();
		}
		public void setSize(Dimension d) {
			if (d.width <= getParent().getSize().width) {
				d.width = getParent().getSize().width;
			}
			super.setSize(d);
		}
		public boolean getScrollableTracksViewportWidth() {
			Component parent = this.getParent();
			ComponentUI ui = this.getUI();
			return (ui.getPreferredSize(this).width <= parent.getSize().width);
		}
	}
	/**
	 * This class realizes anchor.
	 * @author tzolotuhin
	 */
	private static class Anchor {
		private final String anchorKey;
		private final String anchorValue;
		private final int beginPosition;
		private final int endPosition;
		/**
		 * Constructor.
		 */
		public Anchor(final String anchorKey, final String anchorValue, final int beginPosition, final int endPosition) {
			this.anchorKey = anchorKey;
			this.anchorValue = anchorValue;
			this.beginPosition = beginPosition;
			this.endPosition = endPosition;
		}
		public String getAnchorKey() {
			return(this.anchorKey);
		}
		public String getAnchorValue() {
			return(this.anchorValue);
		}
		public int getBeginPosition() {
			return(this.beginPosition);
		}
		public int getEndPosition() {
			return(this.endPosition);
		}
	}
}
